import createMapProvider from './Helper/createMapProvider'
import RenderHooks from './Helper/RenderHooks'
import GlowStage from './Material/GlowStage'

export default class Sandbox {
  constructor(elemId, { msaaSamples, fxaa, glowStage, skyBox, shadows, shouldAnimate, backgroundColor }) {
    this.viewer = new GC.Viewer(elemId, {
      animation: true,
      fullscreenButton: false,
      baseLayerPicker: false,  // 禁用底图选择器
      vrButton: false,
      geocoder: false,  // 禁用搜索框
      homeButton: false, // 禁用主页按钮
      infoBox: false,  // 禁用信息框（点击实体时的弹窗）
      sceneModePicker: false, // 禁用 2D/3D 切换按钮
      selectionIndicator: false, // 禁用选择指示器（高亮框）
      timeline: false,  // 禁用时间轴
      navigationHelpButton: false,
      navigationHelpButton: false,
      // baseLayer: false,
      msaaSamples: GC.defaultValue(msaaSamples, 1),
      skyBox: GC.defaultValue(skyBox, false),
      shadows: GC.defaultValue(shadows, true),
      shouldAnimate: GC.defaultValue(shouldAnimate, true),
      pickTranslucentDepth: false, // 默认false，设为true可pick透明物体，但降低性能
      glowStage,
      baseLayer: false
      // contextOptions: {
      //   webgl: { alpha: true }
      // },
      // useBrowserRecommendedResolution: false
      // terrainShadows: GC.ShadowMode.ENABLED
    })
    const { scene } = this.viewer
    const { globe } = scene
    this.scene = scene
    this.globe = globe
    scene.light = new GC.DirectionalLight({
      direction: new GC.Cartesian3(1, 0, -1),
      // color: GC.Color.fromCssColorString('#FFF'),
      intensity: 1
    })
    // 背景色
    scene.backgroundColor = GC.Color.fromCssColorString(GC.defaultValue(backgroundColor, 'rgba(0,0,0,1)'))
    // 快速近似抗锯齿
    this.viewer.scene.postProcessStages.fxaa.enabled = GC.defaultValue(fxaa, false)
    // 设置阴影
    const { shadowMap } = this.viewer
    shadowMap.softShadows = true
    shadowMap.maximumDistance = 5000
    // 自发光
    if (glowStage) {
      this.glowStage = new GlowStage(scene)
      this.glowStage.intensity = 1.0
      this.glowStage.render()
    }

    // 在判断拾取到的overlay时分别代表地面、三维场景
    this.globeSymbol = Symbol('globe')
    this.sceneSymbol = Symbol('scene')
    // 取消双击聚焦
    this.viewer.screenSpaceEventHandler.removeInputAction(GC.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)
    this.registerEventHandler()
    this.watchPostRender()
    // // 等待IndexedDB就绪后返回
    // return scene.dbPromise.then(() => {
    //   return this
    // })
  }

  async setMapLayer(params, keep = false) {


    const provider = await createMapProvider(params)
    const imageryLayers = this.viewer.imageryLayers
    if (!keep) {
      imageryLayers.removeAll()
    }

    imageryLayers.addImageryProvider(provider)
    if (params.note) {
      const noteProvider = await createMapProvider({
        type: 'tianditu_note',
        tk: params.tk,
        maximumLevel: params.maximumLevel
      })
      imageryLayers.addImageryProvider(noteProvider)
    }
  }

  clearMapLayer() {
    this.viewer.imageryLayers.removeAll()
  }

  async loadTerrain(url) {
    if (!url) return
    const terrainProvider = await GC.CesiumTerrainProvider.fromUrl(url)
    this.viewer.terrainProvider = terrainProvider
  }

  removeTerrain() {
    this.viewer.terrainProvider = new GC.EllipsoidTerrainProvider()
  }

  // TODO datasource需要额外处理？
  registerEventHandler() {
    const scene = this.scene
    // 事件和回调函数存储在eventMap中，三层Map结构  eventMap={事件类型：{绑定对象：{回调函数：配置项}}}
    // 事件名称到原生名称的映射
    const eventTypeDic = {
      click: GC.ScreenSpaceEventType.LEFT_CLICK,
      rightClick: GC.ScreenSpaceEventType.RIGHT_CLICK,
      doubleClick: GC.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
      middleClick: GC.ScreenSpaceEventType.MIDDLE_CLICK,
      leftDown: GC.ScreenSpaceEventType.LEFT_DOWN,
      leftUp: GC.ScreenSpaceEventType.LEFT_UP
    }
    this.eventMap = new Map()
    const handler = new GC.ScreenSpaceEventHandler(scene.canvas)
    for (const eventType of Object.keys(eventTypeDic)) {
      this.eventMap.set(eventType, new Map())
      handler.setInputAction(e => {
        if (!this.eventMap.get(eventType).size) return
        const winXY = e.position
        const pos = this.getPosFromScene(winXY)
        const markerMap = this.eventMap.get(eventType)
        // 处理sandbox上target为scene的事件，与其他事件不冲突，可以共存
        if (markerMap.has(this.sceneSymbol)) {
          const callbackMap = markerMap.get(this.sceneSymbol)
          for (const callbackFunc of callbackMap.keys()) {
            callbackFunc({
              target: 'scene',
              ...pos,
              screenX: winXY.x,
              screenY: winXY.y
            })
          }
        }
        const pickedObject = scene.pick(winXY)
        const overley = this.getPickOverlay(pickedObject)
        if (overley && markerMap.has(overley)) {
          const callbackMap = markerMap.get(overley)
          for (const callback of callbackMap.keys()) {
            const params = {
              target: overley === this.globeSymbol ? 'globe' : overley,
              pickedObject,
              ...pos,
              screenX: winXY.x,
              screenY: winXY.y
            }
            // if (overley[this.reflectSymbol]) {
            //   const property = overley[this.reflectSymbol].propertyMap.get(pickedObject.id.id)
            //   params.property = property
            // }
            callback(params)
          }
        }
      }, eventTypeDic[eventType])
    }
    // mouseenter/mouseleave/mousemove的处理
    this.eventMap.set('mouseenter', new Map())
    this.eventMap.set('mouseleave', new Map())
    this.eventMap.set('mousemove', new Map())
    let lastOverlay = null
    let lastFeature = null
    handler.setInputAction(e => {
      if (!this.eventMap.get('mouseenter').size && !this.eventMap.get('mouseleave').size && !this.eventMap.get('mousemove').size) {
        return
      }
      const winXY = e.endPosition
      const pos = this.getPosFromScene(winXY)
      const pickedObject = scene.pick(winXY)
      const overlay = this.getPickOverlay(pickedObject)
      // 处理mouseleave
      if (!overlay || lastOverlay !== overlay) {
        const markerMap = this.eventMap.get('mouseleave')
        if (lastOverlay && markerMap.has(lastOverlay)) {
          const callbackMap = markerMap.get(lastOverlay)
          for (const callback of callbackMap.keys()) {
            const params = {
              target: lastFeature
            }
            callback(params)
          }
        }
        lastOverlay = null
      }
      // 处理mousemove
      if (overlay) {
        const markerMap = this.eventMap.get('mousemove')
        if (markerMap.has(this.sceneSymbol)) {
          const callbackMap = markerMap.get(this.sceneSymbol)
          for (const callbackFunc of callbackMap.keys()) {
            callbackFunc({
              target: 'scene',
              pickedObject,
              ...pos,
              screenX: winXY.x,
              screenY: winXY.y
            })
          }
        }
        if (markerMap.has(overlay)) {
          const callbackMap = markerMap.get(overlay)
          for (const callback of callbackMap.keys()) {
            const params = {
              target: pickedObject,
              pickedObject,
              ...pos,
              screenX: winXY.x,
              screenY: winXY.y
            }
            callback(params)
          }
        }
      }
      // 处理mouseenter
      if (overlay && overlay !== lastOverlay) {
        lastFeature = pickedObject
        lastOverlay = overlay
        const markerMap = this.eventMap.get('mouseenter')
        if (markerMap.has(overlay)) {
          const callbackMap = markerMap.get(overlay)
          for (const callback of callbackMap.keys()) {
            const params = {
              target: pickedObject,
              pickedObject
            }
            callback(params)
          }
        }
      }
    }, GC.ScreenSpaceEventType.MOUSE_MOVE)
  }

  // 从pickedObject获取overlay
  getPickOverlay(pickedObject) {
    let mapKey = null
    if (pickedObject) {
      if (pickedObject.id && pickedObject.id instanceof GC.Entity) {
        mapKey = pickedObject.id
        // if (this.dataSourceCollection.has(pickedObject.id?.entityCollection.owner)) { // DataSource
        //   mapKey = pickedObject.id.entityCollection.owner
        // } else { // Entity
        //   mapKey = pickedObject.id
        // }
      } else if (pickedObject.primitive) { // Primitive or 3DTileset
        mapKey = pickedObject.primitive
      }
    } else { // 判定点击到地面
      mapKey = this.globeSymbol
    }
    return mapKey
  }

  on(type, callback, option) {
    let target = this.sceneSymbol
    if (option && option.target === 'globe') {
      target = this.globeSymbol
    }
    const handlerOfType = this.eventMap.get(type)
    if (handlerOfType) {
      if (!handlerOfType.has(target)) {
        handlerOfType.set(target, new Map())
      }
      const callbackMap = handlerOfType.get(target)
      callbackMap.set(callback, { option })
    } else {
      console.error('事件类型有误')
    }
    return callback
  }

  off(type, callback, option) {
    let target = this.sceneSymbol
    if (option && option.target === 'globe') {
      target = this.globeSymbol
    }
    if (type) {
      const handlerOfType = this.eventMap.get(type)
      if (handlerOfType) {
        if (callback) {
          const callbackMap = handlerOfType.get(target)
          if (callbackMap) {
            if (callbackMap.has(callback)) {
              callbackMap.delete(callback)
            }
          }
        } else {
          handlerOfType.delete(target)
        }
      } else {
        console.error('事件类型错误')
      }
    } else {
      this.eventMap.forEach(handlerOfType => {
        handlerOfType.delete(target)
      })
    }
  }

  get cameraPos() {
    const { camera } = this.viewer
    const cartographic = camera.positionCartographic
    const lat = GC.Math.toDegrees(cartographic.latitude).toFixed(8) - 0
    const lng = GC.Math.toDegrees(cartographic.longitude).toFixed(8) - 0
    const alt = cartographic.height.toFixed(3) - 0
    const pitch = GC.Math.toDegrees(camera.pitch).toFixed(2) - 0
    const heading = GC.Math.toDegrees(camera.heading).toFixed(2) - 0
    const roll = GC.Math.toDegrees(camera.roll).toFixed(2) - 0
    return {
      lng, lat, alt, pitch, heading, roll
    }
  }

  get cameraFocus() {
    const { canvas } = this.viewer.scene
    const winCenter = new GC.Cartesian2(canvas.clientWidth / 2.0, canvas.clientHeight / 2.0)
    const position = this.getPosFromScene(winCenter)
    if (position && position.alt >= 0) {
      return position
    }
    return this.getPosFromGlobe(winCenter)
  }

  // scene.pickPosition(xy)在pick到globe时，结果无效，此时需要使用getPosFromGlobe的结果
  getPosFromScene(xy) {
    const scene = this.scene
    const pickedObject = scene.pick(xy)
    if (pickedObject) {
      const position = scene.pickPosition(xy)
      if (position) {
        const { lng, lat, alt } = this.cartesianToDegrees(position)
        // if (alt < 0) {
        //   console.warn('DEBUG: alt < 0') // 若确实有地下内容，则alt允许小于0
        // }
        return {
          lng, lat, alt, x: position.x, y: position.y, z: position.z
        }
      }
    } else {
      return this.getPosFromGlobe(xy)
    }
  }

  getPosFromGlobe(xy) {
    let position
    if (this.viewer.terrainProvider instanceof GC.CesiumTerrainProvider) {
      // 在地形上pick
      const ray = this.camera.getPickRay(xy)
      if (!ray) {
        console.warn('DEBUG: no camera ray')
        return
      }
      position = this.scene.globe.pick(ray, this.scene)
    } else { // 在椭球上pick
      position = this.camera.pickEllipsoid(xy)
    }
    if (position) {
      const { lng, lat, alt } = this.cartesianToDegrees(position)
      if (alt < 0 && !this.viewer.terrainProvider) {
        console.warn('DEBUG: error position from globe') // 在globe上pick不允许alt小于0
      }
      return { lng, lat, alt, ...position }
    } else {
      console.warn('DEBUG: no valid position')
    }
  }

  lngLatToAlt(position) {
    const cartographic = GC.Cartographic.fromDegrees(+position.lng, +position.lat)
    const height = this.viewer.scene.sampleHeight(cartographic)
    return height
  }

  degreesToWindow(pos) {
    const cartesian = GC.Cartesian3.fromDegrees(+pos.lng, +pos.lat, +pos.alt)
    return this.viewer.scene.cartesianToCanvasCoordinates(cartesian)
  }

  cartesianToDegrees({ x, y, z }) {
    const { ellipsoid } = this.viewer.scene.globe
    const position = new GC.Cartesian3(x, y, z)
    const cartographic = ellipsoid.cartesianToCartographic(position)
    const lat = GC.Math.toDegrees(cartographic.latitude).toFixed(8) - 0
    const lng = GC.Math.toDegrees(cartographic.longitude).toFixed(8) - 0
    const alt = cartographic.height.toFixed(3) - 0
    return { lng, lat, alt }
  }

  watchPostRender() {
    this.renderHooks = new RenderHooks()
    this.scene.postRender.addEventListener(() => {
      this.renderHooks.actions.forEach(cb => {
        if (typeof cb === 'function') {
          cb()
        }
      })
    })
  }

  setView({
    lng, lat, alt, pitch = 0, heading = 0, roll = 0
  }) {
    const pitchInRadians = GC.Math.toRadians(pitch)
    const headingInRadians = GC.Math.toRadians(heading)
    const rollInRadians = GC.Math.toRadians(roll)
    this.viewer.camera.setView({
      destination: GC.Cartesian3.fromDegrees(+lng, +lat, +alt),
      orientation: {
        heading: headingInRadians,
        pitch: pitchInRadians,
        roll: rollInRadians
      }
    })
  }

  flyTo({
    lng, lat, alt, pitch = 0, heading = 0, roll = 0, duration = 3, stay = 0, entity, offset, easingFunction
  }) {
    return new Promise((resolve) => {
      if (entity?.overlay) {
        this.viewer.flyTo(entity.overlay, offset
          ? {
            offset: new GC.HeadingPitchRange(
              GC.Math.toRadians(offset.heading),
              GC.Math.toRadians(offset.pitch),
              offset.range)
          }
          : null)
      } else {
        this.viewer.camera.flyTo({
          destination: GC.Cartesian3.fromDegrees(+lng, +lat, +alt),
          orientation: {
            heading: GC.Math.toRadians(heading),
            pitch: GC.Math.toRadians(pitch),
            roll: GC.Math.toRadians(roll)
          },
          duration,
          easingFunction: easingFunction || (time => {
            return Math.sin(time * Math.PI / 2)
          }),
          complete: () => {
            stay
              ? setTimeout(() => {
                resolve()
              }, stay * 1000)
              : resolve()
          }
        })
      }
    })
  }

  // TODO 测试效果
  // 线性视角平移
  linearFlyTo({
    lng, lat, alt, pitch = 0, heading = 0, roll = 0, duration = 3, stay = 0
  }) {
    return new Promise(resolve => {
      const start = this.cameraPos
      const frames = duration * 60
      const diffLng = lng - start.lng
      const diffLat = lat - start.lat
      const diffAlt = alt - start.alt
      const diffHeading = heading - start.heading
      const diffPitch = pitch - start.pitch
      const diffRoll = roll - start.roll
      const step = {
        lng: diffLng / frames,
        lat: diffLat / frames,
        alt: diffAlt / frames,
        heading: diffHeading / frames,
        pitch: diffPitch / frames,
        roll: diffRoll / frames
      }
      let i = 0
      const moveCamera = () => {
        const position = {
          lng: start.lng + step.lng * i,
          lat: start.lat + step.lat * i,
          alt: start.alt + step.alt * i,
          heading: start.heading + step.heading * i,
          pitch: start.pitch + step.pitch * i,
          roll: start.roll + step.roll * i
        }
        this.setView(position)
        i += 1
        if (i >= frames) {
          this.viewer.scene.preRender.removeEventListener(moveCamera)
          stay
            ? setTimeout(() => {
              resolve()
            }, stay * 1000)
            : resolve()
        }
      }
      this.flyToEventListener = moveCamera
      this.viewer.scene.preRender.addEventListener(moveCamera)
    })
  }

  cancelLinearFly() {
    if (this.flyToEventListener) {
      this.viewer.scene.preRender.removeEventListener(this.flyToEventListener)
    }
  }

  async orbitFly({ lng, lat, alt, x, y, z, speed }) {
    const camera = this.viewer.camera
    const target = GC.Cartesian3.fromDegrees(lng, lat, alt)
    const ground = GC.Cartesian3.fromDegrees(lng, lat, 0)
    const topDirection = GC.Cartesian3.normalize(
      GC.Cartesian3.subtract(target, ground, new GC.Cartesian3()),
      new GC.Cartesian3()
    )
    const modelMatrix = GC.Transforms.eastNorthUpToFixedFrame(target)
    const wc = GC.Matrix4.multiplyByPoint(
      modelMatrix,
      new GC.Cartesian3(x, y, z),
      new GC.Cartesian3()
    )
    const cameraDirection = GC.Cartesian3.normalize(
      GC.Cartesian3.subtract(target, wc, new GC.Cartesian3()),
      new GC.Cartesian3()
    )
    await camera.flyTo({
      destination: wc,
      orientation: {
        direction: cameraDirection,
        up: topDirection
      }
    })

    camera.lookAt(target, new GC.Cartesian3(x, y, z))
    const diff = Math.PI / 360 * speed
    this.orbitFlying = true
    const rotate = () => {
      if (this.orbitFlying) {
        camera.rotateLeft(diff)
        requestAnimationFrame(rotate)
      } else {
        camera.lookAtTransform(GC.Matrix4.IDENTITY)
      }
    }
    rotate()
  }

  cancelOrbitFly() {
    this.orbitFlying = false
  }

  // 完全隐藏地球
  hideGlobe() {
    const { sun, moon, skyBox, globe, underGlobe } = this.scene
    const arr = [sun, moon, skyBox, globe, underGlobe]
    arr.forEach(i => {
      if (i) {
        i.show = false
      }
    })
    if (underGlobe) {
      underGlobe.baseColor = new GC.Color(0, 0, 0, 0)
    }
    if (globe) {
      globe.baseColor = new GC.Color(0, 0, 0, 0)
    }
    this.viewer.scene.undergroundMode = true
    this.viewer.scene.backgroundcolor = new GC.Color(0, 0, 0, 0)
  }

  recoverGlobe() {
    const { sun, moon, skyBox, globe, underGlobe } = this.viewer.scene
    const arr = [sun, moon, skyBox, globe, underGlobe]
    arr.forEach(i => {
      if (i) {
        i.show = true
      }
    })
    if (underGlobe) {
      underGlobe.baseColor = new GC.Color(0, 0, 0, 0)
    }
    if (globe) {
      globe.baseColor = new GC.Color(0, 0, 0, 0)
    }
    this.viewer.scene.undergroundMode = false
    this.viewer.scene.backgroundcolor = new GC.Color(0, 0, 0, 0)
  }

  get camera() {
    return this.viewer.camera
  }

  get clock() {
    return this.viewer.clock
  }

  get domElem() {
    return this.viewer.container
  }

  get canvas() {
    return this.viewer.canvas
  }

  get globeRotation() {
    return this._globeRotation
  }

  set globeRotation(bool) {
    if (this._globeRotation && !bool) {
      this.stopRotate()
    } else if (!this._globeRotation && bool) {
      this.startRotate()
    }
    this._globeRotation = bool
  }

  rotateGlobe() {
    const { camera, clock } = this
    const currentTime = clock.currentTime.secondsOfDay
    const delta = (currentTime - this.previousTime) / 10
    this.previousTime = currentTime
    camera.rotate({ x: 0, y: 0, z: 1 }, -delta)
  }

  // 开始转
  startRotate() {
    this.clock.shouldAnimate = true
    this.previousTime = this.clock.currentTime.secondsOfDay
    this.thisRotateGlobe = this.rotateGlobe.bind(this)
    this.clock.onTick.addEventListener(this.thisRotateGlobe)
  }

  // 停止转
  stopRotate() {
    if (this.thisRotateGlobe) {
      this.clock.onTick.removeEventListener(this.thisRotateGlobe)
    }
  }

  get maximumCameraHeight() {
    return this.scene.screenSpaceCameraController.maximumZoomDistance
  }

  set maximumCameraHeight(value) {
    if (!isNaN(value)) {
      this.scene.screenSpaceCameraController.maximumZoomDistance = value
    }
  }

  get minimumCameraHeight() {
    return this.scene.screenSpaceCameraController.minimumZoomDistance
  }

  set minimumCameraHeight(value) {
    if (!isNaN(value)) {
      this.scene.screenSpaceCameraController.minimumZoomDistance = value
    }
  }

  addPostProcess(p) {
    if (!p.stage) return
    this.viewer.scene.postProcessStages.add(p.stage)
  }

  removePostProcess(p) {
    if (!p.stage) return
    this.viewer.scene.postProcessStages.remove(p.stage)
  }
}
